#!/usr/bin/perl

################################################################################
#
#	SERIALSERVER
#
#	Modified from aquosserver which was:
#       Created by Ronald Frazier
#       http://www.ronfrazier.net
#
#	Modified by Wayne Thomas to support LG Television, Denon Amplifier,
#	and Marantz Amplifier devices.
#       Feel free to do anything you want with this, as long as you
#       include the above attribution.
#
#	Todo:	Alter CMD and INPUT commands to select correct device
#
################################################################################

use strict;
use IO::Socket::INET;
use SerialLibs::IOSelectBuffered;
use SerialLibs::Sharp;
use SerialLibs::LG;
use SerialLibs::Denon;
use SerialLibs::Marantz;
use Getopt::Long;

my $listenport = 4684;

# Television Equipment Settings

my $use_tv_vol = 'yes';
my $use_sharp = 'no';
my $use_lg = 'yes';
my $tv_port = '/dev/ttyUSB0';

# Stereo/Amplifier Equipment Settings

my $use_denon = 'no';
my $use_marantz = 'no';
my $amp_port = '/dev/ttyS0';

$|=1;

my $sharp = 0;
my $lgtv = 0;
my $denon = 0;
my $marantz = 0;

my $help = '';
my $info = '';

# Process any options passed in
#
GetOptions (    'help' =>       	\$help,
                'info' =>       	\$info,
                'tcp_port=i' =>     	\$listenport,
		'use_tv_vol=s' =>	\$use_tv_vol,
                'tv_port=s' =>     	\$tv_port,
		'amp_port=s' =>		\$amp_port,
                'use_lg=s' =>    	\$use_lg,
                'use_sharp=s' =>    	\$use_sharp,
                'use_denon=s' =>    	\$use_denon,
                'use_marantz=s' =>    	\$use_marantz);
if ($help) { help(); exit 0;}
if ($info) { info(); exit 0;}
foreach my $check ($use_tv_vol, $use_sharp, $use_lg, $use_denon, $use_marantz)
{
	die ("Illegal value for use_XXX variable.  Valid entries are 'yes' or 'no' only.  Stopped")
		unless (($check eq 'yes') || ($check eq 'no'));
}

$sharp = new SerialLibs::Sharp($tv_port, $use_tv_vol) if ($use_sharp eq 'yes');
$lgtv = new SerialLibs::LG($tv_port, $use_tv_vol) if ($use_lg eq 'yes');
$denon = new SerialLibs::Denon($amp_port) if ($use_denon eq 'yes');
$marantz = new SerialLibs::Marantz($amp_port) if ($use_marantz eq 'yes');

die ("You are attempting to control two amplifiers, this is not allowed, aborted") if ($denon && $marantz);
die ("You are attempting to control two televisions, this is not allowed, aborted") if ($sharp && $lgtv);

die ("TV serial port could not be initiated correctly, aborted") if ((($use_sharp eq 'yes') || ($use_lg eq 'yes')) && !($sharp || $lgtv));
die ("Amp serial port could not be initiated correctly, aborted") if ((($use_denon eq 'yes') || ($use_marantz eq 'yes')) && !($denon || $marantz));


my $listensocket = new IO::Socket::INET ( 
	LocalPort => $listenport,
	Proto => 'tcp',
	Listen => 1,
	Reuse => 1,
	Blocking => 1
	);
die ("Could not create socket: $!") unless $listensocket;


my $select = new SerialLibs::IOSelectBuffered($listensocket);

LOOP : while(my @ready = $select->can_read())
{
	return unless scalar(@ready);
	foreach my $handle (@ready)
	{
		#open a socket for the newly connected client
		if ($handle == $listensocket)
		{
			my $client = $listensocket->accept();
			$client->autoflush(1);
			$select->add($client);

			next;
		};

		#read the next line and test for error, EOF, socket disconnect, or just partial lines
		my $line = $select->getline($handle);
		if (!defined($line))
		{
			$select->remove($handle);
			$handle->close();
			next;
		};
		next if $line eq '';

		$line =~ s/[\n\r\f]+$//ms;

		if ($line =~ /EXIT/i)
		{
			$select->remove($handle);
			$handle->close();
			next;
		};

		foreach my $currentport ($sharp, $denon, $marantz, $lgtv)
		{
			if ($currentport)
			{
				my $result = processLine($line, $currentport);
				syswrite($handle, "$result\n") unless ($result eq 'NOP');
			}
		}

		if ($line =~ /KILL/i)
		{
			syswrite($handle, "CLOSING DOWN SERIALSERVER, GOODBYE\n");
			$select->remove($handle);
			$handle->close();
			last LOOP;
		};

	};
};
exit;
	
sub processLine
{
	my ($line, $device) = @_;
	$line = uc $line;

	if ($line =~ /^AMP[_ ]?(.*)/i)
	{
		return "NOP" if ($device == $sharp || $device == $lgtv);
		$line = $1;
	};
	if ($line =~ /^TV[_ ]?(.*)/i)
	{
		return "NOP" if ($device == $denon || $device == $marantz);
		$line = $1;
	};

	if ($line =~ /^KILL[_ ]?(.*)/i)
	{
		my $dead = $device->kill;
		die ("could not close serial port, aborted") unless $dead;
		return "SERIAL PORT CLOSED";
	};

	my $zone = 0;
	if ($line =~ /^ZONE[_ ]?(.*)/i)
	{
		return "NOP" if ($device == $sharp || $device == $lgtv);
		$line = $1;
		$zone = 1;
	};

	if ($line =~ /^VOL[_ ]?(.*)/i)
	{
		my $param = $1;
		if ($param eq '+')
		{
			return $device->adjust_volume(1, $zone);
		}
		elsif ($param eq '-')
		{
			return $device->adjust_volume(-1, $zone);
		}
		elsif ($param =~ /^\d+$/)
		{
			return $device->volume($param, $zone);
		}
		else
		{
			return $device->volume('', $zone);
		};
	}
	elsif ($line =~ /^MUTE[_ ]?(.*)/i)
	{
		my $param = $1;
		if ($param =~ /ON/i)
		{
			return $device->mute(1, $zone);
		}
		elsif ($param =~ /OFF/i)
		{
			return $device->mute(0, $zone);
		}
		elsif ($param =~ /TOGGLE/i)
		{
			return $device->toggle_mute($zone);
		}
		else
		{
			return $device->mute('', $zone);
		};
	}
	elsif ($line =~ /^POWER[_ ]?(.*)/i)
	{
		my $param = $1;
		if ($param =~ /ON/i)
		{
			return $device->power(1, $zone);
		}
		elsif ($param =~ /OFF/i)
		{
			return $device->power(0, $zone);
		}
# I have purposely not implemented power toggle for any of the new equipment
		elsif ($param =~ /TOGGLE/i)
		{
			if ($device == $sharp && $zone == 0)
			{
				my $state = $device->power() ? 0 : 1;
				return $device->power($state);
			}
			else
			{
				return "NOP";
			};
		}
		else
		{
			return $device->power('', $zone);
		};
	}
	elsif ($line =~ /^INPUT[_ ]?(.*)/i)
	{
		my $param = $1;
		return $device->input($param, $zone);
	}
	elsif ($line =~ /^SERIALPOWERUP[_ ]?(.*)/i)
	{
		if ($device == $sharp)
		{
			my $param = $1;
			if ($param =~ /ENABLE/i)
			{
				return $device->enable_serial_powerup(1);
			}
			elsif ($param =~ /DISABLE/)
			{
				return $device->enable_serial_powerup(0);
			}
			else
			{
				#this doesn't seem to work
				return $device->enable_serial_powerup();
			};
		}
		else
		{
			return "NOP";
		};
	}
	elsif ($line =~ /^DEBUG[_ ]?(.*)/i)
	{
		my $param = $1;
		if ($param ne '')
		{
			return $device->debug($param);
		}
		else
		{
			return $device->debug();
		};
	}
	elsif ($line =~ /^CMD[_ ]?(.{4})\s*(.*)/i)
	{
		my $code = $1;
		my $param = $2;
		if ($param ne '')
		{
			return $device->set($code, $param);
		}
		else
		{
			return $device->get($code);
		};
	}
	else
	{
		return "Invalid Command";
	};

};

# Display help page
sub help
{
        print   "\n	==============================================\n";
        print   "	serialserver - RS-232 Control of AV Equipment.\n";
        print   "	==============================================\n\n";
        print   "Usage:		serialserver [Options] [&]\n\n";
        print   "Options:	--help		Display this page\n";
        print   "		--info		Display info regarding serialserver\n";
#        print   "		--verbose	Turn on verbose output\n";
        print   "		--tcp_port	TCP Port to listen for commands on (default: 4684)\n";
        print   "		--tv_port	TV device location (default: /dev/ttyUSB0)\n";
        print   "		--amp_port	Amp device location (default: /dev/ttyS0)\n";
	print   "		--use_tv_vol	Use TV volume control (can be disabled if not using TV for audio) (default: yes)\n";
	print   "		--use_lg	Control a LG Television (default: yes)\n";
	print   "		--use_sharp	Control a Sharp Television (default: no)\n";
	print   "		--use_denon	Control a Denon Amplifier (default: no)\n";
	print   "		--use_marantz	Control a Marantz Amplifier (default: no)\n";
        print   "&:		Used to daemonise serialserver\n\n";
        print   "Example:	serialserver --use_lg=no --use_denon=yes --amp_port=/dev/ttyS0 --tcp_port=1221 &\n\n";
        print   "For more information execute 'serialserver --info'\n\n";
        return;
}

# Display Info page
#
sub info
{
	print   "\n	==============================================\n";
	print   "	serialserver - RS-232 Control of AV Equipment.\n";
	print   "	==============================================\n\n";
	print   "Created by Wayne Thomas.\n\n";
	print   "Listens on the designated TCP port for correctly formatted commands and passes them to the\n";
	print   "appropriate equipment, then returns the response.\n\n";
	print   "       Based upon aquosserver.pl (written by Ron Frazier - http://www.ronfrazier.net).\n\n";
	print   "Latest version and discussion, see http://code.google.com/p/serialserver-for-linux/\n";
	print   "History, see http://code.google.com/p/serialserver-for-linux/source/list\n\n";
	print   "Feel free to do anything you want with this, as long as you include the above attribution.\n\n";
        print   "Interact with the controller from any network connect pc using netcat in the following manner:\n\n";
	print	"	nc localhost 4684\n\n";
	print	"Change localhost and 4684 to the appropriate hostname (or IP address) and port accordingly.\n";
	print	"Note that for some Linux installations, 'nc' has to replaced with 'netcat'.\n\n\n";
        print   "Valid Commands:\n";
	print	"===============\n\n";
	print   "	POWER state	==> state = ON, OFF, TOGGLE (Sharp TV only), or blank (for query)\n";
	print   "	VOL value	==> value = 0 - 100, or blank (for query)\n";
	print   "	MUTE state	==> state = ON, OFF, TOGGLE, or blank (for query)\n";
	print   "	INPUT component	==> component name (see below for list of valid names) or blank (for query)\n";
	print   "	DEBUG level	==> level = INFO, WARN, ERROR, NONE\n";
	print   "	CMD command	==> raw command, not implemented\n";
	print   "	SERIALPOWERUP	==> Sharp TV only, enable serial power on\n";
	print   "	EXIT		==> closed current TCP connection to server\n";
	print   "	KILL		==> close all connections and shutdown serialserver\n\n";
	print   "Additionally, for Denon and Marantz Amplifiers that that have multiple audio zones you can control\n";
	print   "a second zone by adding \"ZONE\" to the relevant commands.\n";
	print   "	For example:	\"VOL 50\" will adjust the volume of the primary zone to 50%\n";
	print   "			\"VOL ZONE 40\" will adjust the volume of the secondary zone to 40%\n\n";
	print   "Valid Inputs:\n";
	print	"=============\n\n";
	print   "	LG Televisions - DTV, ATV, AV1, AV2, AV3, COMP1, COMP2, PC, HDMI1, HDMI2, HDMI3, HDMI4, OSDMUTE\n";
	print   "	(note: OSDMUTE is not an actual input, but instead inhibits the TV OSD i.e during volume changes)\n\n";
	print   "	Sharp Televisions - ???????\n\n";
	print   "	Denon Amplifiers - PHONO, CD, TUNER, DVD, VDP, TV, DBS/SAT, VCR-1, VCR-2, V.AUX, CDR/TAPE1\n\n";
	print   "	Marantz Amplifiers - TV, DVD, VCR1, DSS/VCR2, AUX1, AUX2, CD, CD-R, TAPE, TUNER, FM, AM, XM\n\n\n";
	print   "Commands can be sent from scripts easily by piping the output of echo to netcat. For example:\n";
	print   "	echo -e 'POWER ON\\nEXIT\\n' | nc localhost 4684\n\n";
	print   "If you are using a slow remote connection use netcat's -i option to increase the interval:\n";
	print   "	echo -e 'POWER ON\\nEXIT\\n' | nc -i 1 othernetworkhost 4684\n\n";
	return;
}
